SVG-Based Program Milestone Reporting User Manual
This document intends to help the user configure gantt_ui_config.json to produce Gantt Chart with desired visual effects.
gantt_ui_config.json
Renderer: Render_gantt_svg.py (modified 2026-04-27 15:29:16)
1. Introduction
The renderer produces a single, publication quality SVG that is designed to be pasted straight into a PowerPoint slide. The whole look and feel of that SVG is governed by one file, gantt_ui_config.json.
Every change you want to make to the chart, from switching to a T minus countdown, to widening the swim lane labels, to making the arrows dashed, to rescaling everything to fit a 16 by 9 slide, is a JSON edit and a rerun. No fight with Office drawing objects.
2. At a glance
A snapshot of the current configuration so you know what you are working with before you start tweaking:
the current configuration
- Data source mode
- Mode B - Cache drives the SVG mode-b
- Canvas sizing
- custom 2680 x 720 px
- Visible window
2026-03-15to2026-05-20, spanweekly(66 days, roughly 9 weekly buckets)- T-minus axis
- enabled, baseline 2026-05-22, unit weeks
- Swim lanes
- 7 lanes pinned in order: SDD, Telecomms, Infrastructure, M&E and BMS, SCSs Issued ...
- Overlap handling
- default vertical, with 4 per-lane override(s): Test Assurance & Risk Assessment, Configuration Document(SDD), Configuration Deep Dives ...
- Arrows
- stroke 1.8 px, dasharray none, opacity 0.85
- Milestone labels
- exterior labels ON
3. How the pipeline works
Before touching any settings, it helps to understand what the script actually does when you run it. There are two data paths, selected by data_source.source. Everything else in the config affects layout, not data.
Four output files can appear in the working folder:
gantt_output_YYYYMMDD_HHMMSS.svg, the chart itself, timestamped so you never overwrite an older one by accident.gantt_data.json, the cache that holds every visible milestone and every dependency arrow in a plain, hand readable form.gantt_data_audit.html, a data quality report that flags dangling predecessor references, missing end dates, date inversions, and so on.gantt_excel_json_diff.mht, only produced in Mode B. A side by side comparison of what is in the cache versus what is in the live Excel.- This manual file itself, regenerated on every run to stay in sync with the config.
4. data_source
This block decides where the milestone data comes from. It is the single most important switch in the file, because it determines whether a run will touch the Excel or not.
the current data_source block
{
"source": "json",
"_source_comment": "Mode A ('Excel') extracts from the workbook. Mode B ('json') reads from the cache for rapid layout iteration.",
"excel_file_name": "2026.04.21 Technical - Entry Criteria Program.xlsx",
"_excel_file_name_comment": "Path to the source Excel file. Can be relative or absolute.",
"json_file_name": "gantt_data.json",
"_json_file_name_comment": "Cache file written by the tool.",
"excel_sheet": "Schema Normalisation",
"_excel_sheet_comment": "Target worksheet containing the data."
}
Mode A, source = "Excel"
This is production mode. The script opens the workbook, reads the sheet named in excel_sheet, extracts every row that has a Function Group and a valid End Date that falls inside the timeline window, writes those rows out to the JSON cache, runs the data quality audit, and then renders the SVG.
Mode A is what you want at the end of every planning cycle. It takes Excel, the team's single source of truth, and rolls it forward into the JSON cache.
Mode B, source = "json"
This is layout iteration mode, and it is the reason this tool exists in its current form. Instead of reading the Excel, the renderer loads gantt_data.json directly and ignores the workbook for input purposes.
Why is that useful? Because it lets you play with the data without touching the source of truth. You can:
- Change a milestone date to see how the layout rebalances before anyone else sees it.
- Insert a brand new swim lane to mock up a workshop that has not been agreed yet.
- Delete a stale predecessor arrow to see if the chart still reads clearly.
- Rename an activity to something shorter just so the exterior label fits.
- Copy and paste a milestone into a different swim lane to audition a reorg.
Then, once the chart looks right, you run the script one more time still in Mode B. Because the Excel is still present on disk, the script will read it in parallel to the cache and produce gantt_excel_json_diff.mht. That file is a side by side table showing every milestone whose cache value differs from the Excel, highlighted in amber. You take that report, open Excel, and type exactly those changes into the cells. That keeps Excel authoritative without forcing you to edit Excel every time you want to try a layout idea.
gantt_data.json until the SVG looks right, read the diff MHT, mirror the diffs into Excel, then flip back to Mode A and run once more to regenerate the cache cleanly.
The other three keys
| Key | What it does |
|---|---|
excel_file_name | Path to the workbook. Can be relative (resolved next to the script) or absolute. Only consulted in Mode A, or in Mode B when a diff report is being produced. |
json_file_name | Path to the cache. In Mode A it is written. In Mode B it is read. |
excel_sheet | Exact worksheet tab name to read from. If not found, the script falls back to Schema Normalisation, then to the active sheet. |
5. svg_dimensions
This block decides the physical size of the SVG canvas. It has one key that matters above all others, fit_mode, and two numeric keys that only take effect when fit_mode is set to "custom".
the current svg_dimensions block
{
"fit_mode": "custom",
"_fit_mode_comment": "Use 'custom' to define exact WxH, or 'natural' to scale based on contents.",
"custom_width_px": 2680,
"_custom_width_px_comment": "The exact canvas width in pixels.",
"custom_height_px": 720,
"_custom_height_px_comment": "The exact canvas height in pixels.",
"preserve_aspect_ratio": "xMidYMid meet",
"_preserve_aspect_ratio_comment": "Keeps the aspect ratio proportional when pasting into PowerPoint."
}
fit_mode
Five values are recognised. The renderer uses the target size to work backwards and compute a new column_width_px that will make the chart fill the canvas horizontally. In other words, you do not choose both the canvas width and the column width, you choose one and the other is derived.
| Value | Target width | Target height | When to use |
|---|---|---|---|
natural | sum of columns | sum of lanes | You want each column to be exactly column_width_px wide, and you do not care if the chart ends up wider or narrower than the slide. |
custom | custom_width_px | custom_height_px | You know the exact pixel budget. This is the most common choice for PowerPoint. |
powerpoint_16_9 | 1280 | 720 | Standard widescreen slide at standard def. |
powerpoint_16_9_hd | 1920 | 1080 | Standard widescreen slide at full HD. |
powerpoint_4_3 | 960 | 720 | Legacy 4 by 3 decks. |
a4_landscape | 1169 | 826 | A4 at 100 dpi for print. |
fit_mode is anything other than natural, the renderer computes col_w = (target_width - label_width) / number_of_columns. So if you have a 1680 pixel target, a 220 pixel swim lane label column, and 10 weekly columns, each column will be rendered at 146 pixels wide, regardless of what you put in timeline.column_width_px.
preserve_aspect_ratio
This is an SVG attribute that controls what happens when the SVG is embedded in a container of a different aspect ratio (for example, a PowerPoint placeholder that is not exactly 1680 by 720). The default "xMidYMid meet" keeps the whole chart visible, centred, with any leftover space as margin. It is almost always the right setting. Alternatives are "xMidYMid slice", which crops to fill, and "none", which stretches.
6. timeline
The timeline block controls what time period the chart covers, how time is bucketed into columns, and the baseline column width before the fit_mode override kicks in.
the current timeline block
{
"span": "weekly",
"_span_comment": "Bucketing logic. Options: weekly, monthly, quarterly.",
"start_date": "2026-03-15",
"_start_date_comment": "Start of the chart's visible window.",
"end_date": "2026-05-20",
"_end_date_comment": "End of the chart's visible window.",
"column_width_px": 110,
"_column_width_px_comment": "Base width of one time column. Overridden dynamically if fit_mode = custom."
}
span
Three values are accepted, "weekly", "monthly", and "quarterly". The renderer snaps the start date to the beginning of the chosen bucket and then walks forward one bucket at a time until it passes the end date.
start_date and end_date
These two dates define the visible window of the chart. Any milestone whose end date falls inside this window is rendered. Anything outside is silently dropped, with a tally in the summary block of gantt_data.json under skipped_out_of_bounds. This is how you narrow a chart down to, say, just the next two months of activity even when the Excel plan covers two years.
end_date is kept. A milestone one day later is not.
column_width_px
The baseline width in pixels of a single bucket. This is the value that is used when svg_dimensions.fit_mode = "natural". For any other fit mode, the column width is overridden at render time so the chart exactly fills the chosen canvas width, and this key is effectively ignored. Keep it sensible anyway, because Mode natural is a useful debugging fallback.
7. relative_time_axis
This adds a secondary header row between the Year row and the Time row. Instead of showing calendar dates, it shows a countdown or count up relative to a reference date that you choose. This is how you get the familiar "T minus 8 weeks" style on an executive chart.
the current relative_time_axis block
{
"enabled": true,
"_enabled_comment": "If true, adds a secondary T-minus header row.",
"baseline_date": "2026-05-22",
"_baseline_date_comment": "The T+0 anchor date.",
"unit": "weeks",
"_unit_comment": "Unit for the countdown (weeks, months, quarters).",
"zero_label": "T+0",
"_zero_label_comment": "Text to show precisely on the baseline date.",
"format_positive": "T+{n}",
"_format_positive_comment": "Format string for dates after the baseline.",
"format_negative": "T-{n}",
"_format_negative_comment": "Format string for dates before the baseline."
}
How the countdown is computed
The renderer walks every column in the timeline and asks "how many units between this column and the baseline?" For weeks it snaps both dates to their respective Mondays first, so the answer is always a clean integer. For months it uses year and month arithmetic. For quarters it uses year and quarter arithmetic.
Then it feeds that integer into format_positive or format_negative, substituting {n} with the absolute value. On the exact baseline column it shows zero_label verbatim instead.
Changing the unit
When you change unit, you usually want to change span to match, otherwise the middle row will have repeated values inside a single bucket.
| span | Recommended unit |
|---|---|
| weekly | weeks |
| monthly | months |
| quarterly | quarters |
enabled is false, the middle row disappears entirely and its pixel budget is removed from the total header height. Nothing else in the chart shifts around.
8. node_styling (diamonds and labels)
This block governs everything about how each milestone is drawn. It is divided into two parts: the diamond itself, and the exterior text label that sits next to it.
the current node_styling block
{
"diamond_size_px": 20,
"_diamond_size_px_comment": "The size (radius) of the milestone diamond.",
"diamond_stroke_color": "#333333",
"_diamond_stroke_color_comment": "Border color around the diamond.",
"diamond_stroke_width_px": 1.5,
"_diamond_stroke_width_px_comment": "Thickness of the diamond's border.",
"diamond_text_display_for_all_diamonds": "yes",
"_diamond_text_display_comment": "Set to 'yes' to show exterior milestone activity labels, or 'no' to hide them.",
"label_wrap_chars": 22,
"_label_wrap_chars_comment": "Wrap activity text onto a new line if it exceeds this character length.",
"label_font_size_px": 22,
"_label_font_size_px_comment": "Font size for the milestone description text.",
"label_font_color": "#1F2937",
"_label_font_color_comment": "Dark elegant gray text. Maps to 'text_dark'.",
"label_gap_px": 25,
"_label_gap_px_comment": "Distance between the diamond tip and the description text.",
"label_line_height_px": 28,
"_label_line_height_px_comment": "Vertical distance between wrapped text lines."
}
Diamond geometry
| Key | Effect |
|---|---|
diamond_size_px | Half the diagonal of the diamond, measured in pixels. A value of 14 means the diamond is 28 pixels tall and 28 pixels wide. Increasing it makes every milestone more prominent but also forces the swim lane to grow taller. |
diamond_stroke_color | HEX colour of the border drawn around the diamond. The fill of the diamond itself comes from the swim lane colour. |
diamond_stroke_width_px | Thickness of that border in pixels. A value of 0 removes the border. |
The label toggle
diamond_text_display_for_all_diamonds is the single most impactful switch in this block. Set it to "yes" and every diamond gets a multi line activity label drawn below or above it. Set it to "no" and the diamonds stand on their own, stripped of all text.
Label typography
| Key | Effect |
|---|---|
label_wrap_chars | Approximate character count per line. The wrapper greedily packs whole words until adding one more would exceed the limit. |
label_font_size_px | Pixel size of the activity text. 14 to 16 pixels reads well at slide distance. |
label_font_color | Any HEX. The default dark grey reads clearly on white and does not fight with the brightly coloured diamonds. |
label_gap_px | Vertical distance between the bottom tip of the diamond and the top of the first line of text. |
label_line_height_px | Vertical distance between consecutive wrapped lines. |
9. arrow_styling
This block controls every dependency arrow in the chart. Arrows are routed orthogonally, meaning they only ever travel horizontally or vertically in ninety degree turns. The arrow colour is not controlled here; each arrow inherits the colour of the swim lane the predecessor sits in.
the current arrow_styling block
{
"stroke_width_px": 1.8,
"_stroke_width_px_comment": "Line weight (thickness) of the connection arrows. Change this from 5,3 to none ",
"stroke_dasharray": "none",
"_stroke_dasharray_comment": "Line type: '5,3' makes a dashed line. Set to 'none' or '' for a solid line.",
"opacity": 0.85,
"_opacity_comment": "Transparency of the arrow line (1.0 = fully opaque).",
"marker_width": 8,
"_marker_width_comment": "Size of the arrowhead pointing at the target milestone.",
"marker_height": 6,
"_marker_height_comment": "Height of the arrowhead."
}
stroke_width_px
The thickness of the line. Values under 1 look like ghosts on a projector. Values above 3 start to visually compete with the diamonds. The sweet spot for a slide is 1.5 to 2.
stroke_dasharray
This is the single key that switches the arrow between solid and dashed. The value is an SVG stroke dash pattern. The first number is the length of the ink segment. The second number is the length of the gap.
opacity
A number from 0 to 1. The default of 0.85 is a subtle choice, it keeps the line clearly visible but softens it just enough that on a slide with a lot of arrows, the lines recede a touch and let the diamonds dominate.
marker_width and marker_height
These two keys control the arrowhead triangle that sits at the end of every dependency line. marker_width is how long the triangle is in the direction of travel. marker_height is how thick it is perpendicular to travel.
10. overlap_handling
When two or more milestones in the same swim lane fall close enough in time that their diamonds would overlap horizontally, the renderer has to decide what to do. This block defines that behaviour globally, and the swimlanes.overrides block lets specific lanes deviate.
the current overlap_handling block
{
"default_mode": "vertical",
"_default_mode_comment": "Strategy to handle overlaps: 'vertical' (stacks them on top of each other) or 'horizontal' (spreads them wide).",
"collision_width_px": 140,
"_collision_width_px_comment": "Horizontal hit-box used to detect if two diamonds are colliding.",
"vertical": {
"spacing_per_level_px": 45,
"_spacing_per_level_px_comment": "How far down a stacked milestone drops to avoid collision."
},
"horizontal": {
"min_diamond_gap_px": 55,
"_min_diamond_gap_px_comment": "Minimum horizontal gap between spread-out diamonds.",
"alternate_label_side": true,
"_alternate_label_side_comment": "If true, labels zig-zag above and below the baseline to avoid each other.",
"label_offset_above_px": 28,
"_label_offset_above_px_comment": "How high above the baseline top-labels sit.",
"label_offset_below_px": 12,
"_label_offset_below_px_comment": "How low below the baseline bottom-labels sit.",
"fallback_to_vertical_after_n": 5,
"_fallback_to_vertical_after_n_comment": "If a cluster exceeds this count, starts stacking vertically too."
}
}
collision_width_px
This is the hit box. Two diamonds are considered to be colliding if they are within this many pixels of each other horizontally. A larger value is more aggressive, making the overlap logic kick in earlier.
default_mode = "vertical"
Under vertical mode, when a collision is detected, the later diamond drops down one "level" where each level is spacing_per_level_px pixels deep. Stacks can get arbitrarily tall, and the swim lane expands automatically to contain them.
default_mode = "horizontal"
Horizontal mode keeps diamonds on the same baseline and spreads them outwards along the time axis. It is particularly good for lanes where there are three or four milestones in a tight cluster.
min_diamond_gap_pxis the enforced minimum horizontal separation after the spread.alternate_label_side, when true, alternates exterior labels above and below the baseline so they zigzag.label_offset_above_pxis how far above the diamond centre the label sits when placed above.label_offset_below_pxis the equivalent for the below case.fallback_to_vertical_after_nis the safety net. If more than N diamonds are in a single cluster, horizontal mode gives up and switches to vertical for that cluster.
11. swimlanes
The swimlanes block controls the left hand label column, the vertical order of the lanes, and per lane overrides for overlap mode.
the current swimlanes block
{
"lane_order": [
"SDD",
"Telecomms",
"Infrastructure",
"M&E and BMS",
"SCSs Issued",
"CBR",
"Risk Assessments"
],
"_lane_order_comment": "Explicit top-to-bottom order. Missing lanes are appended alphabetically.",
"label_width_px": 220,
"_label_width_px_comment": "Width of the left-hand column containing the swim lane titles.",
"label_font_size_px": 20,
"label_line_height_px": 38,
"_label_font_size_px_comment": "Font size of the swim lane titles.",
"label_font_color": "#FFFFFF",
"_label_font_color_comment": "Color of the swim lane titles.",
"label_wrap_chars": 24,
"_label_wrap_chars_comment": "Wrap long swim lane names at this character limit.",
"top_padding_px": 40,
"_top_padding_px_comment": "Distance from the top boundary of the lane to the diamonds.",
"bottom_padding_px": 20,
"_bottom_padding_px_comment": "Empty space below the diamonds in a lane.",
"fallback_color": "#4472C4",
"_fallback_color_comment": "Color used if the Excel 'WBS Colour' column is blank.",
"row_fill_uses_lane_color": true,
"_row_fill_uses_lane_color_comment": "If true, the swim lane row background uses the lane's specific color instead of the page background.",
"row_shade_opacity": 0.15,
"_row_shade_opacity_comment": "Opacity of the row background fill (e.g., 0.05 for a very light tint, 0.0 for transparent).",
"alternate_column_shading": true,
"_alternate_column_shading_comment": "If true, every second timeline column has no background tint, creating a vertical striped effect.",
"overrides": {
"Test Assurance & Risk Assessment": {
"overlap_mode": "horizontal"
},
"Configuration Document(SDD)": {
"overlap_mode": "horizontal"
},
"Configuration Deep Dives": {
"overlap_mode": "horizontal"
},
"QR RCCP Approval": {
"overlap_mode": "horizontal"
}
}
}
lane_order
An ordered list of Function Group names. Lanes listed here appear top to bottom in the chart in exactly this order. Any swim lane present in the data but not listed here is appended at the bottom, alphabetically.
The left label column
Every lane has its label painted in a solid coloured block on the left, label_width_px pixels wide. The colour of the block is the lane colour, which is derived from the WBS Colour Code column in Excel.
top_padding_px and bottom_padding_px
These are the vertical cushions inside every lane. top_padding_px is the distance from the upper boundary of the lane to the diamonds on level 0. bottom_padding_px is the empty space below the last line of label text.
custom_height_px target, the renderer distributes the leftover vertically, padding each lane equally to fill the canvas.
Row Shading and Striping
The configuration provides options to subtly tint the background of the timeline grid to make reading across the chart easier:
| Key | Effect |
|---|---|
row_fill_uses_lane_color | If set to true, the horizontal row spanning the timeline will inherit the swim lane's specific color (instead of the default page background). |
row_shade_opacity | Controls the transparency of the row fill. Values should be kept very low (e.g., 0.05 to 0.15) so the tint is subtle and does not overpower the milestone diamonds. Set to 0.0 for completely invisible rows. |
alternate_column_shading | If set to true, every odd-indexed time column drops the background fill entirely. This creates a vertical striped effect (like a checkerboard) making it much easier to track dates vertically down a tall chart. |
overrides
A per lane escape hatch for overlap mode. If the global overlap_handling.default_mode is set to vertical but you have one lane where horizontal spread reads better, add an entry here pointing that single lane name at "overlap_mode": "horizontal".
12. header_styling
The header is built from up to three stacked rows. From top to bottom: the year row, the optional T minus row, and the time row. Each row has its own height, background, font size, and font colour.
the current header_styling block
{
"height_year_px": 38,
"_height_year_px_comment": "Height of the top-most (Year) row.",
"year_bg": "#002855",
"_year_bg_comment": "Dark elegant blue. Maps to 'header_bg'.",
"year_font_color": "#FFFFFF",
"_year_font_color_comment": "Text color in the year row.",
"year_font_size_px": 26,
"_year_font_size_px_comment": "Font size for the year text.",
"height_t_minus_px": 32,
"_height_t_minus_px_comment": "Height of the middle T-minus row.",
"t_minus_bg": "#002855",
"_t_minus_bg_comment": "Background of the T-minus row. Set identical to 'year_bg' for a unified block.",
"t_minus_font_color": "#FFFFFF",
"_t_minus_font_color_comment": "Text color in the T-minus row.",
"t_minus_font_size_px": 22,
"_t_minus_font_size_px_comment": "Font size for the T-minus text.",
"height_time_px": 46,
"_height_time_px_comment": "Height of the bottom (Months/Weeks) header row.",
"time_bg": "#002855",
"_time_bg_comment": "Background for the bottom header row.",
"time_font_color": "#FFFFFF",
"_time_font_color_comment": "Text color for the bottom header row.",
"time_font_size_px": 22,
"_time_font_size_px_comment": "Font size for the bottom header row."
}
13. page
The catch all for chart wide styling.
the current page block
{
"background_color": "#FFFFFF",
"_background_color_comment": "The base color behind the entire chart. Maps to 'bg'.",
"border_color": "#E5E7EB",
"_border_color_comment": "Color of the outer border box. Maps to 'border'.",
"border_width_px": 2,
"_border_width_px_comment": "Thickness of the outer border.",
"font_family": "Arial, sans-serif",
"_font_family_comment": "CSS font family applied to all text in the SVG."
}
| Key | Effect |
|---|---|
background_color | The colour behind everything. Usually white for print and for slides with white backgrounds. |
border_color | Colour of the outer chart border. |
border_width_px | Thickness of the outer border. Set to 0 for no border. |
font_family | CSS font family string, applied to every piece of text in the SVG. |
14. reports
the current reports block
{
"audit_html": "gantt_data_audit.html",
"_audit_html_comment": "Output filename for data quality review.",
"diff_mht": "gantt_excel_json_diff.mht",
"_diff_mht_comment": "Output filename for version control comparisons.",
"user_manual_html": "User Manual - SVG-Based Program Milestone Reporting.html",
"_user_manual_html_comment": "Output filename for the regenerated HTML user manual."
}
File names for the three reports the script can produce: the data quality audit HTML, the Excel versus JSON diff MHT, and this user manual HTML. All three can be absolute paths if you want them written somewhere other than next to the script.
The audit HTML
Generated every time the script reads Excel. For every milestone in the sheet it flags missing predecessors, dangling references, date inversions, and predecessors that point at rows without end dates.
The diff MHT
Generated only in Mode B. It walks every milestone in the timeline window and compares the cached JSON value to the freshly read Excel value for five fields: Function Group, End Date, Activity, WBS Colour, and Predecessors. Any differing cell is highlighted amber.
The user manual HTML
The file you are reading now. Regenerated automatically on every run, so the code blocks above always reflect the live config. If you are writing a handover document, this is the one file that is always current.
15. Recipes
15.1 Fit the chart to a 1280 by 720 PowerPoint slide
"svg_dimensions": {
"fit_mode": "powerpoint_16_9"
}
15.2 Switch from weekly to quarterly with matching T minus
"timeline": { "span": "quarterly" },
"relative_time_axis": { "unit": "quarters" }
15.3 Remove the T minus row entirely
"relative_time_axis": { "enabled": false }
15.4 Strip every exterior label off the diamonds
"node_styling": { "diamond_text_display_for_all_diamonds": "no" }
15.5 Make every arrow dashed for a proposed plan
"arrow_styling": {
"stroke_dasharray": "5,3",
"opacity": 0.7
}
15.6 Force one lane to spread horizontally while everything else stacks vertically
"swimlanes": {
"overrides": {
"Configuration Deep Dives": { "overlap_mode": "horizontal" }
}
}
15.7 Narrow the visible window to the next six weeks
"timeline": {
"start_date": "2026-04-24",
"end_date": "2026-06-05",
"span": "weekly"
}
15.8 Make the chart presentable in monochrome printing
"node_styling": { "diamond_stroke_width_px": 2.5 },
"arrow_styling": { "opacity": 1.0, "stroke_width_px": 2.4 }
16. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Chart is empty | No milestone dates fall inside the timeline window | Widen timeline.start_date and timeline.end_date, or check _summary.skipped_out_of_bounds. |
| A lane appears in the wrong order | The lane name in lane_order does not exactly match Excel | Copy the Function Group value from Excel and paste it into lane_order verbatim. |
| A predecessor arrow is missing | Either the predecessor is outside the window, or the Dependencies column has a typo | Open the audit HTML. Dangling references are flagged in red. |
| Diamonds overlap and look cramped | Overlap mode is not resolving them because they are not within collision_width_px | Increase overlap_handling.collision_width_px to trigger the resolver sooner. |
| Chart is wider than the slide | fit_mode is set to natural | Switch to custom or one of the PowerPoint presets. |
| Arrows look faint on projector | Default opacity of 0.85 plus a thin stroke | Bump opacity to 1.0 and stroke_width_px to 2.2. |
| Script aborts with a lock message | Excel workbook is open | Close the workbook and rerun. The Mode B cache is unaffected. |
| The diff MHT never appears | You are running in Mode A, or the Excel path is wrong | Diff is Mode B only. Confirm data_source.source = "json" and the Excel file is reachable. |
SVG-Based Program Milestone